/*
    Geocache: some utilities for managing and visualizing geocache information
    Copyright (C) 2008  Gary Jackson

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
/*
 * CommandLine.java
 */

package com.thegbomb.common;

import java.io.PrintStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

/**
 *
 * @author Gary Jackson
 */
public class CommandLine {
    private boolean extra = false;
    
    public CommandLine(boolean extra) {
        this.extra = extra;
    }
    
    public interface Action {
        public void act(String option, String argument) throws UsageException;
        public boolean isRequired();
        public boolean isArgument();
        public boolean isMultiplicity();
    }
    
    public static class UsageException extends Exception {
        public UsageException(String msg) {
            super(msg);
        }
    }
    
    public static class TypeMismatch extends UsageException {
        public TypeMismatch(String option, String argument, String expected) {
            super("expected type " + expected + " for option " + option + ", could not convert " + argument);
        }
    }
    
    public static class MissingArgument extends UsageException {
        public MissingArgument(String option) {
            super("expected an argument for option " + option);
        }
    }
    
    public static class RequiredOptionMissing extends UsageException {
        public RequiredOptionMissing(String option) {
            super("required option " + option + " is missing");
        }
    }
    
    public static class OnlyOnce extends UsageException {
        public OnlyOnce(String option) {
            super("option " + option + " can be used only once");
        }
    }
    
    public static class UnrecognizedOption extends UsageException {
        public UnrecognizedOption(String option) {
            super("option " + option + " is not recognized");
        }
    }
    
    public static class ExtraArgument extends UsageException {
        public ExtraArgument(String argument) {
            super("extra argument \"" + argument + "\" not allowed");
        }
    }
    
    private Map<String, Action> map = new HashMap<String,Action>();
    private Set<String> required = new HashSet<String>();
    
    public List<String> process(String commandLine[]) throws UsageException {
        Set<String> used = new HashSet<String>();
        List<String> ret = new LinkedList<String>();
        
        Pattern isOption = Pattern.compile("^-.*");
        int index = 0;
        while (index < commandLine.length) {
            String option = commandLine[index++];
            
            // If there is no option, just add it to the bare return list
            if (! isOption.matcher(option).matches()) {
                if (extra) {
                   ret.add(option);
                   continue;
                } else {
                    throw new ExtraArgument(option);
                }
            }
            
            Action action = map.get(option);
            if (action == null)
                throw new UnrecognizedOption(option);
            
            if (!action.isMultiplicity() && used.contains(option))
                throw new OnlyOnce(option);
            
            String argument = "";
            if (action.isArgument()) {
                if (index == commandLine.length)
                    throw new MissingArgument(option);
                argument = commandLine[index++];
                if (isOption.matcher(argument).matches())
                    throw new MissingArgument(option);
            }
            
            action.act(option, argument);
            
            used.add(option);
        }
        
        for (String s: required)
            if (!used.contains(s))
                throw new RequiredOptionMissing(s);
        
        return ret;
    }
    
    public void printUsage(PrintStream out, String arg0, UsageException exception) {
        if (exception != null)
            out.println("Command Line Error: " + exception.getMessage());
        
        out.println("Usage:");
        StringBuffer main = new StringBuffer("\t").append(arg0).append(' ');
        for (Entry<String, Action> e: map.entrySet()) {
            StringBuffer buf = new StringBuffer(e.getKey());
            if (e.getValue().isArgument())
                buf.append(" <").append(e.getKey().substring(1).toUpperCase()).append('>');
            if (e.getValue().isMultiplicity())
                buf.append(" ...");
            if (! e.getValue().isRequired())
                buf.insert(0, '[').append(']');
            main.append(buf).append(' ');
        }
        if (extra)
            main.append("<ARG> ... <ARG>");
        out.println(main);
    }
    
    public void addAction(String name, Action action) {
        map.put("-" + name, action);
        if (action.isRequired())
            this.required.add("-" + name);
    }
}
